<?php
//see README for examples and skeleton docs
// xml2array package 

//cbueno 12 Sep 2005 -- added the X2A object to make OOP people happy
//todo: bug: CDATA is not appearing in $parent_info.

//cbueno 21 Sep 2005 -- fixed bug where '<tag>0</tag>' was not being parsed.
// credit to goes to pkelaita from the Strategic Data Storage team.
// (PHP lesson for the day: the string "0" evaluates to false.)

// makes a path string from an array of tag names. $level lets you leave off
// leading tags, and $end lets you tack one more name on the end.
function xmlhelper__j($path, $level=0, $end='') {
	$path = array_slice($path, $level);
	if ($end) {
		array_push($path, $end);
	}
	return join($path, '/');
}


//handles the messy instantiation and teardown of the builtin xml parser
function __raw_array($xml) {
    $vals = array();
    $xml_parser = xml_parser_create();
    xml_parse_into_struct($xml_parser, $xml, $vals, $index);
    xml_parser_free($xml_parser);
    return $vals;
}

//outputs an array of 'tag paths', one for each 'rowtag'. It is handy to 
// print_r() the output of this or xml2map() to get an idea of how it
// converts your XML to an array.
## todo: hackish and semi-documented.
function __xml2array($xml, $rowtag, $include_siblings=false, $raw_array=null) {
    if(!is_array($raw_array)) {
        $vals = __raw_array($xml);
    } else {
        $vals = $raw_array;
    }
	//print_r($vals);
	/*
    xml_parse_into_struct() returns a list of xml tokens in the order they
    appear in the document. It includes fun facts such as the type
    (open tag, close tag, complete tag, etc), level, value, and any attributes.
    We iterate over this list to build a flattened 2-d array
	*/

	
	$path = array();         //the current tag's 'path'
	$index = 0;
	$level = 0;              //also known as count(path);
	$ret = array();          //the return value
	$parent_info = array();  //retains attrs and data from tags above the rowtag.
	$rootlevel = 0;          //really should be called $rowtag_level. 
	$inside_rowtag = false;  //are we examining a subtag of our rowtag?
	
	foreach($vals as $e) {
		//establish the depth at which our 'rowtag' first appears in the XML.
		if(!$rootlevel and $e['tag'] == $rowtag) {
			$rootlevel = $e['level'];
		}
	
		if($e['type'] != 'close') { //open, complete, cdata, etc
		
			//XXX: added to exclude info from *siblings* of the
			//rowtag. this is arguably bad & is optional using the $include_siblings
			// argument... be careful, though... the prospects for truly spectacular
			// garbage output are good. (maybe we could have named siblings?) 
			if (($e['type'] == "open" or $e['type'] == "complete") and $e['tag'] == $rowtag) {
				$inside_rowtag = true;			
			}
		
			//keep track of the 'path'
			if($e['level'] > $level) {
				$path[] = $e['tag'];
				$level++;
			} elseif ($e['level'] < $level) {
				array_pop($path);
				$level--;
			}
			
			// this is a fun part. if the $level is 'above' the $rowtag, store
			// any values into the $parent_info array. this will get pasted
			// onto each 'row'
			if(!$rootlevel or ($e['level'] <= $rootlevel and $e['tag'] != $rowtag)) {
				if (isset($e['attributes']) and is_array($e['attributes'])) {
					foreach($e['attributes'] as $a=>$v) { 
						$parent_info[xmlhelper__j($path, $e['level']-1, $a)] = $v;
					}
                    //todo: bug: what about CDATA for the parent info?
				}
			} elseif($include_siblings or $inside_rowtag) { //regular tag
				if (isset($e['attributes']) and is_array($e['attributes'])) {
					foreach($e['attributes'] as $a=>$v) { 
						$p = xmlhelper__j($path, $rootlevel, $a);
						
						//more special-case fun. if the attr repeats, we 
						//make that key's value an array, AND make new subkeys:
						//   /FOO/BAR => array('a', 'b'),
						//	 /FOO/BAR/0 => 'a',
						//	 /FOO/BAR/1 => 'b'
						//
						if(isset($ret[$index][$p])) {
							if(is_array($ret[$index][$p])) {
								$ret[$index][$p.'/'.count($ret[$index][$p])] = $v;
								$ret[$index][$p][] = $v;
							} else {
								$ret[$index][$p.'/0'] = $ret[$index][$p];
								$ret[$index][$p.'/1'] = $v;
								$ret[$index][$p] = array($ret[$index][$p], $v);
							}
						} else {
							$ret[$index][$p] = $v;
						}
					}

                }
				
				//'cdata' type, complete type, etc.
				$p = xmlhelper__j($path, $rootlevel);

				//does this path already exist?
                if (isset($ret[$index][$p])) {
                    if (is_array($ret[$index][$p])) {
                        $ret[$index][$p.'/'.count($ret[$index][$p])] = (isset($e['value']) ? $e['value'] : '');
                        $ret[$index][$p][] = (isset($e['value']) ? $e['value'] : '');
                    } else {
                        $ret[$index][$p.'/0'] = $ret[$index][$p];
                        $ret[$index][$p.'/1'] = (isset($e['value']) ? $e['value'] : '');
                        $ret[$index][$p] = array($ret[$index][$p], (isset($e['value']) ? $e['value'] : ''));
                    }
                  
                //new path -- just store the cdata
                } else {
                    $ret[$index][$p] = (isset($e['value']) ? $e['value'] : '');
                }
			}

            // looking for the ending of an instance of our 'rowtag'. If we are there,
            // append the parent info, incr the index and pop out of 'inside_rowtag' status.
            if ($e['type'] == 'complete' and $e['tag'] == $rowtag and $level == $rootlevel) {
				$ret[$index] += $parent_info; //tack on 'parent' info to this row
				$index++;
				$inside_rowtag = false;
			}
			
		//closing the 'rowtag'
		} elseif($e['tag'] == $rowtag and $level == $rootlevel) {
			if (!is_array($ret[$index])) { $ret[$index] = array(); }
			$ret[$index] += $parent_info; //tack on 'parent' info to this row
			$index++;
			$inside_rowtag = false;
	
		}
	}
	return $ret;
}

function mk_column_name($path) {
	return strtolower(array_pop(explode("/", $path)));
}

//see xml2map()
function __xml2map($xml, $rowtag) {
	$map = array();
	$rows = __xml2array($xml, $rowtag); //grab the raw "paths"
	foreach($rows as $r) {				//guess at the column names
		foreach(array_keys($r) as $k) {
			$map[$k] = mk_column_name($k);
		}
	}
	return $map;
}

//helper function for writing maps. you give it a sample document and the
//'rowtag' you want to use, and it returns a skeleton of the map array you'll
// need for xml2array();
function xml2map($xml, $rowtag) {
	$ret = array();
	$map = __xml2map($xml, $rowtag);
	foreach($map as $m=>$v) {
		$ret[] = "'$m' => '$v'";
	}
	return "\$map = array(\n\t" . join($ret, ",\n\t") . "\n);\n";
}

//helper function for making SQL CREATE TABLE statments from an
// xml map
function map2sql($map, $rowtag) {
	$ret = array();
	//$map = __xml2map($xml, $rowtag);
	foreach($map as $m=>$v) {
		$ret[] = "$v varchar(32)";
	}
	return "create table $rowtag (\n\t" . join($ret, ",\n\t") . "\n);\n";
}

//the main show.
##todo: can maps be nested?? why not?
function xml2array($xml, $rowtag, $map, $raw_array=null) {
	$ret = array();

    //grab the raw "paths"
	$rows = __xml2array($xml, $rowtag, false, $raw_array); 

    //remap the paths to the given names
	foreach($rows as $r) {              
		$temp = array();
		foreach($map as $old_key=>$new_key) {
          if(!isset($r[$old_key])) { continue; }
			$temp[$new_key] = $r[$old_key];
		}
		$ret[] = $temp;
	}
	return $ret;
}



// a class wrapper for the xml2array functions with an optimization.
// if you find you are calling xml2array() on several tags in the same document,
// use this interface because it caches the 'raw' output from the xml parser.
// performance profiling is left as an exercise for the reader.
//todo: array2insert()? xml2sql()?
class X2A {
  function X2A($xml) {
    $this->raw_array = __raw_array($xml);
  }
  function to_array($rowtag, $map) {
    return xml2array(null, $rowtag, $map, $this->raw_array);
  }
}


?>